// Package db 提供了数据库相关的工具函数，包括获取表结构信息、创建扫描目标以及格式化数据进行校验。
package db

import (
	"database/sql" // 标准数据库接口
	"errors"       // 用于创建标准错误
	"fmt"          // 用于格式化字符串和错误
	"strings"      // 用于字符串处理
	"sync"
	"time" // 用于处理时间类型

	// 使用别名导入日志库，避免与标准库 log 冲突
	log "github.com/sirupsen/logrus"
)

// ColumnInfo 存储数据库列的详细信息
type ColumnInfo struct {
	Name            string // 列名 (例如: user_id, created_at)
	DataType        string // 基础数据类型 (小写, 例如: int, varchar, timestamp, bigint)。来自 information_schema.COLUMNS.DATA_TYPE
	OrdinalPosition int    // 列在表中的顺序 (从1开始)
	ColumnType      string // 完整的列类型定义 (小写, 例如: int(11), varchar(255), bigint unsigned, timestamp)。来自 information_schema.COLUMNS.COLUMN_TYPE
	IsNullable      bool   // 列是否允许 NULL 值
	// 添加更多字段以便将来更精细地处理特定类型，例如：
	// CharMaxLength    sql.NullInt64 // 字符类型最大长度
	// NumericPrecision sql.NullInt64 // 数字精度
	// NumericScale     sql.NullInt64 // 小数位数
}

// GetTableColumnsAndTypes 从 information_schema 获取指定表的所有列及其详细类型信息
// 参数:
//   - dbConn: 已打开的数据库连接 (*sql.DB)
//   - schemaName: 数据库模式名
//   - tableName: 表名
//
// 返回值:
//   - []ColumnInfo: 包含所有列信息的切片，按列在表中的顺序排列
//   - error: 如果查询或处理过程中出错，则返回错误
func GetTableColumnsAndTypes(dbConn *sql.DB, schemaName, tableName string) ([]ColumnInfo, error) {
	// 参数验证
	if dbConn == nil {
		return nil, errors.New("数据库连接无效 (nil)")
	}
	if schemaName == "" || tableName == "" {
		return nil, errors.New("数据库名和表名不能为空")
	}

	// 构建查询 information_schema.COLUMNS 的 SQL 语句
	// 添加注释 /* GetTableColumnsAndTypes */ 便于在数据库日志中识别查询来源
	// DATA_TYPE 提供基础类型，COLUMN_TYPE 提供完整定义（包含长度、精度、unsigned 等）
	// IS_NULLABLE 返回 'YES' 或 'NO'
	query := `
		SELECT /* GetTableColumnsAndTypes */
			COLUMN_NAME,
			DATA_TYPE,
			ORDINAL_POSITION,
			COLUMN_TYPE,
			IS_NULLABLE
		FROM information_schema.COLUMNS
		WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
		ORDER BY ORDINAL_POSITION /* 确保按列顺序返回 */
	`

	// 执行查询
	rows, err := dbConn.Query(query, schemaName, tableName)
	if err != nil {
		// 查询失败，返回包含具体表名和原始错误的错误信息
		return nil, fmt.Errorf("查询列信息失败 for %s.%s: %w", schemaName, tableName, err)
	}
	// 确保在函数退出时关闭结果集
	defer rows.Close()

	var columns []ColumnInfo // 用于存储结果的切片
	found := false           // 标记是否至少找到了一行数据

	// 遍历查询结果的每一行
	for rows.Next() {
		found = true // 找到了数据行
		var col ColumnInfo
		// 定义用于接收可能为 NULL 的数据库字段的变量 (information_schema 字段本身不应为 NULL，但使用 NullString 更安全)
		var columnName, dataType, columnType, isNullableStr sql.NullString
		var ordinalPosition sql.NullInt64 // ORDINAL_POSITION 是整数

		// 扫描当前行的数据到变量中
		err := rows.Scan(
			&columnName,      // COLUMN_NAME
			&dataType,        // DATA_TYPE
			&ordinalPosition, // ORDINAL_POSITION
			&columnType,      // COLUMN_TYPE
			&isNullableStr,   // IS_NULLABLE
		)
		if err != nil {
			// 扫描失败，返回错误
			return nil, fmt.Errorf("扫描列信息失败 for %s.%s: %w", schemaName, tableName, err)
		}

		// 处理 NULL 值并赋值给 ColumnInfo 结构体
		if columnName.Valid {
			col.Name = columnName.String
		} else {
			// COLUMN_NAME 为 NULL 几乎不应该发生
			log.Warnf("列 %s.%s 扫描到 COLUMN_NAME 为 NULL", schemaName, tableName)
			col.Name = "UNKNOWN_COLUMN" // 给一个默认名
		}

		if dataType.Valid {
			col.DataType = strings.ToLower(dataType.String) // 转换为小写
		} else {
			// DATA_TYPE 为 NULL
			log.Warnf("列 %s.%s.%s 的 DATA_TYPE 为 NULL", schemaName, tableName, col.Name)
			col.DataType = "unknown"
		}

		if ordinalPosition.Valid {
			col.OrdinalPosition = int(ordinalPosition.Int64)
		} else {
			// ORDINAL_POSITION 为 NULL
			log.Warnf("列 %s.%s.%s 的 ORDINAL_POSITION 为 NULL", schemaName, tableName, col.Name)
			col.OrdinalPosition = 0 // 设为 0 或其他标记值
		}

		if columnType.Valid {
			col.ColumnType = strings.ToLower(columnType.String) // 转换为小写
		} else {
			// COLUMN_TYPE 为 NULL，这可能发生在某些旧版本或特定情况下
			log.Warnf("列 %s.%s.%s 的 COLUMN_TYPE 为 NULL，将使用 DATA_TYPE (%s) 代替", schemaName, tableName, col.Name, col.DataType)
			col.ColumnType = col.DataType // 回退到使用 DATA_TYPE
		}

		// 判断是否允许 NULL，忽略大小写比较 'YES'
		col.IsNullable = isNullableStr.Valid && strings.ToUpper(isNullableStr.String) == "YES"

		// 将处理好的列信息添加到结果切片中
		columns = append(columns, col)
	}

	// 检查遍历过程中是否发生错误 (例如，数据库连接中断)
	if err = rows.Err(); err != nil {
		return nil, fmt.Errorf("处理列信息结果集时出错 for %s.%s: %w", schemaName, tableName, err)
	}

	// 如果没有找到任何行，说明表可能不存在或没有列
	if !found {
		// 更明确的错误信息
		return nil, fmt.Errorf("未找到表 %s.%s 的任何列信息 (表或数据库模式可能不存在或用户没有权限)", schemaName, tableName)
	}

	// 记录成功获取信息的日志
	log.Infof("获取到表 %s.%s 的 %d 列信息", schemaName, tableName, len(columns))
	return columns, nil
}

// processedTypes 是一个集合，用于跟踪已经记录过警告的列类型，避免重复日志。
// key 是 dbTypeFull 字符串。
// 使用 sync.Map 确保并发安全，因为 CreateScanTarget 可能在多个 goroutine 中调用。
var processedTypes sync.Map // 将 map[string]struct{} 改为 sync.Map

// logOnce 辅助函数，用于确保对同一个 dbTypeFull 只记录一次警告 (修改为使用 sync.Map)
func logOnce(dbTypeFull, message string) {
	if _, loaded := processedTypes.LoadOrStore(dbTypeFull, struct{}{}); !loaded {
		// 如果 key 不存在，LoadOrStore 会存储并返回 loaded=false，此时记录日志
		log.Warnf(message)
	}
}

// CreateScanTarget 根据数据库列的完整类型 (ColumnType) 创建合适的 Go 扫描目标变量指针。
// 优化点：为更多数据库类型匹配更合适的 Go 扫描类型，减少对 *sql.NullString 的依赖。
func CreateScanTarget(dbTypeFull string) interface{} {
	// 转换为小写并移除类型定义后面的括号内容（如长度、精度），获取基础类型名称
	dbTypeFullLower := strings.ToLower(dbTypeFull)
	baseType := dbTypeFullLower
	if parenIdx := strings.Index(dbTypeFullLower, "("); parenIdx != -1 {
		baseType = dbTypeFullLower[:parenIdx]
	}

	// 根据基础类型前缀或完整类型判断
	switch {
	// 布尔类型 (通常是 tinyint(1))
	case dbTypeFullLower == "tinyint(1)":
		return new(sql.NullBool)

	// 整数类型 (包括 tinyint, smallint, mediumint, int, integer, bigint)
	// 有符号整数通常在 int64 范围内，使用 *sql.NullInt64
	// 无符号整数，特别是 BIGINT UNSIGNED，可能超出 int64 范围，使用 *sql.NullString 更安全。
	case strings.Contains(baseType, "int"): // 匹配 tinyint, smallint, mediumint, int, bigint, integer
		if strings.Contains(dbTypeFullLower, "unsigned") {
			// 无符号整数（可能超出 int64 范围），使用字符串接收
			return new(sql.NullString)
		}
		// 有符号整数，使用 int64 接收
		return new(sql.NullInt64)

	// 浮点数类型
	case baseType == "float", baseType == "double", baseType == "real":
		return new(sql.NullFloat64)

	// 定点数类型 (高精度)，通常使用字符串接收以保持精度
	case baseType == "decimal", baseType == "numeric", baseType == "dec":
		return new(sql.NullString)

	// 日期和时间类型
	case baseType == "date", baseType == "datetime", baseType == "timestamp":
		// 标准日期时间类型使用 *sql.NullTime。
		return new(sql.NullTime)
	case baseType == "year":
		// YEAR 类型，MySQL 驱动通常返回 int64 或 string
		// 兼容起见，使用 *sql.NullString
		return new(sql.NullString)
	case baseType == "time":
		// MySQL 的 TIME 类型，通常以 "HH:MM:SS" 格式返回字符串
		return new(sql.NullString)

	// 字符串和文本类型，以及一些其他返回字符串的类型 (ENUM, SET, JSON)
	case baseType == "char", baseType == "varchar", baseType == "tinytext", baseType == "text", baseType == "mediumtext", baseType == "longtext", baseType == "enum", baseType == "set", baseType == "json":
		return new(sql.NullString)

	// 二进制和 Blob 类型
	case baseType == "binary", baseType == "varbinary", baseType == "tinyblob", baseType == "blob", baseType == "mediumblob", baseType == "longblob":
		// 这些类型应接收为字节切片 []byte。database/sql 会处理 NULLable 的情况。
		return new([]byte) // 注意：这里返回 *[]byte

	// BIT 类型处理：BIT(1) 在 Go 中通常映射为 bool，其他 BIT(M) 映射为 []byte。
	case baseType == "bit":
		if dbTypeFullLower == "bit(1)" { // 精确匹配 BIT(1)
			return new(sql.NullBool)
		}
		// 其他 BIT(M)
		return new([]byte)

	// 空间数据类型 (Geometry)，通常作为 BINARY 或 BLOB 返回
	case baseType == "geometry":
		return new([]byte)

	// 未知或未显式处理的类型
	default:
		// 对于未知的类型，尝试使用 *sql.NullString 作为通用接收器，并记录警告。
		logOnce(dbTypeFull, fmt.Sprintf("遇到未明确处理的数据库类型 '%s'，将尝试使用 *sql.NullString 接收。", dbTypeFull))
		return new(sql.NullString)
	}
}

// GetScannedValue 从 `database/sql` Scan 函数填充的指针 `ptr` 中提取实际的 Go 值。
// 它处理 `sql.Null*` 类型，将数据库的 NULL 映射为 Go 的 `nil`。
//
// 参数:
//   - ptr: `Scan` 函数填充的指针 (例如 *sql.NullString, *sql.NullInt64)。
//
// 返回值:
//   - interface{}: 提取出的 Go 值 (可能是 nil)。具体类型取决于 CreateScanTarget 的返回类型。
//   - error: 如果指针为 nil 或遇到无法处理的类型，则返回错误。
func GetScannedValue(ptr interface{}) (interface{}, error) {
	if ptr == nil {
		// 理论上 Scan 不会将指针设为 nil，而是将指针指向的值设为 nil 或 Null* 类型无效
		// 但作为防御性编程，检查一下。
		return nil, errors.New("扫描目标指针为 nil")
	}

	// 使用类型断言 (type assertion) 判断指针的具体类型并提取值
	switch v := ptr.(type) {
	case *sql.NullString:
		if v.Valid { // 检查数据库值是否为 NULL
			return v.String, nil // 返回 string 值
		}
		return nil, nil // 数据库为 NULL，返回 Go 的 nil
	case *sql.NullInt64:
		if v.Valid {
			return v.Int64, nil // 返回 int64 值
		}
		return nil, nil
	case *sql.NullBool:
		if v.Valid {
			return v.Bool, nil // 返回 bool 值
		}
		return nil, nil
	case *sql.NullFloat64:
		if v.Valid {
			return v.Float64, nil // 返回 float64 值
		}
		return nil, nil
	case *sql.NullTime:
		if v.Valid {
			return v.Time, nil // 返回 time.Time 值
		}
		return nil, nil
	case *[]byte:
		// 对于 *[]byte 类型，Scan 会将指针 v 指向一个切片。
		// 如果数据库值为 NULL 或空二进制，*v 可能为 nil 或 len 为 0 的切片。
		// 返回时检查 *v 是否为 nil。
		if v == nil || *v == nil { // 检查指针本身是否 nil，再检查指向的切片是否 nil
			return nil, nil // 数据库值为 NULL 或空二进制
		}
		// 如果非 NULL，*v 就是实际的字节切片。返回其值。
		// 理论上应返回拷贝以防止外部修改影响原始数据，但为性能直接返回切片。
		return *v, nil

	// --- 处理其他可能的指针类型（如果 CreateScanTarget 返回了其他类型） ---
	// 尽管 CreateScanTarget 倾向于 Null* 或 *[]byte，保留这些以便未来扩展或应对意外情况
	case *string: // 如果 CreateScanTarget 未使用 sql.NullString
		if v != nil { // 检查指针是否为 nil
			return *v, nil // 返回字符串值
		}
		return nil, nil // 指针为 nil (表示 NULL? 或零值?)
	case *int64: // 如果 CreateScanTarget 未使用 sql.NullInt64
		if v != nil {
			return *v, nil
		}
		return nil, nil
	case *bool: // 如果 CreateScanTarget 未使用 sql.NullBool
		if v != nil {
			return *v, nil
		}
		return nil, nil
	case *float64: // 如果 CreateScanTarget 未使用 sql.NullFloat64
		if v != nil {
			return *v, nil
		}
		return nil, nil
	case *time.Time: // 如果 CreateScanTarget 未使用 sql.NullTime
		if v != nil {
			return *v, nil
		}
		return nil, nil
	// ... 可以根据需要添加更多类型

	// 如果传入的 ptr 不是预期的指针类型
	default:
		// 记录详细错误日志，包含类型信息
		log.Errorf("遇到未处理的扫描目标指针类型: %T", ptr)
		return nil, fmt.Errorf("遇到未处理的扫描目标指针类型: %T", ptr)
	}
}

// GetSQLArgumentValuesFromKeys 将解析后的键值列表转换为适用于 SQL 查询参数的 []interface{}。
// 例如，对于复合键 (col1, col2)，如果 minKeyParts 是 ["abc", nil]，
// 需要将其转换为适合 WHERE (col1, col2) > (?, ?) 子句的参数列表 []interface{}{"abc", nil}。
// ParseKeyFromString 已经将 NULL_KEY_PART 转换为了 Go 的 nil。
// Go 的 database/sql 驱动通常能很好地处理 []interface{} 中包含的 Go 基本类型 (string, int64, bool, float64, time.Time, []byte) 和 nil 值，
// 并将它们正确映射到 SQL 参数类型。
//
// 参数:
//   - keyParts: 从 util.ParseKeyFromString 解析出的键值列表 ([]interface{}).
//
// 返回值:
//   - []interface{}: 适用于 SQL 参数绑定的值切片.
//   - error: 如果转换过程中遇到无法处理的值类型。
//
// 注意：如果 util.ParseKeyFromString 尝试解码十六进制失败，会将值保留为 string。
// 这里的函数直接返回该切片，依赖驱动能将该 string 绑定到 BINARY/BLOB 类型的参数。
// 如果需要更精确的类型转换 (例如将 string "123" 转换为 int64 123)，可以在这里根据列的实际类型进行。
func GetSQLArgumentValuesFromKeys(keyParts []interface{}) ([]interface{}, error) {
	// 大部分情况下，直接返回从 ParseKeyFromString 解析出的 []interface{} 是足够的。
	// 驱动会处理 nil 值，并将 string, []byte 等类型绑定到 SQL 参数。
	// 只有在极少数情况下，驱动无法处理 ParseKeyFromString 返回的特定类型（例如十六进制字符串需要被驱动识别为二进制），
	// 或者需要将 string 显式转换为 Go 数值类型时，才需要在这里进行更复杂的转换。
	// 根据日志和 Go 驱动的行为，直接返回通常是可行的。
	// 如果键值中包含 ParseKeyFromString 无法正确解析的格式（例如包含逗号的字符串键），
	// 那会在 ParseKeyFromString 阶段就返回错误或产生格式化问题，而不是在这里。

	// 简单返回原始切片，依赖数据库驱动进行类型绑定。
	return keyParts, nil
}
